# -*- coding: utf-8 -*-
# @Time    : 2024/7/2 14:01
# @Author  : yujiahao
# @File    : 36_process_thread_2.py
# @description:进程和线程

'''

【多进程还是多线程】

无论是多进程还是多线程，只要数量一多，效率肯定上不去，为什么呢？我们打个比方，假设你不幸正在准备中考，每天晚上需要做语文、数学、英语、物理、
化学这5科的作业，每项作业耗时1小时。如果你先花1小时做语文作业，做完了，再花1小时做数学作业，这样，依次全部做完，一共花5小时，这种方式称为单任务模型。
如果你打算切换到多任务模型，可以先做1分钟语文，再切换到数学作业，做1分钟，再切换到英语，以此类推，只要切换速度足够快，这种方式就和
单核CPU执行多任务是一样的了，以旁观者的角度来看，你就正在同时写5科作业。

但是，切换作业是有代价的，比如从语文切到数学，要先收拾桌子上的语文书本、钢笔（这叫保存现场），然后，打开数学课本、找出圆规直尺（这叫准备新环境），
才能开始做数学作业。操作系统在切换进程或者线程时也是一样的，它需要先保存当前执行的现场环境（CPU寄存器状态、内存页等），然后，把新任务的执行环境准备好
（恢复上次的寄存器状态，切换内存页等），才能开始执行。这个切换过程虽然很快，但是也需要耗费时间。如果有几千个任务同时进行，操作系统可能就主要忙着切换任务，
根本没有多少时间去执行任务了，这种情况最常见的就是硬盘狂响，点窗口无反应，系统处于假死状态。所以，多任务一旦多到一个限度，反而会使得系统性能急剧下降，
最终导致所有任务都做不好。

是否采用多任务的第二个考虑是任务的类型，可以把任务分为计算密集型和I/O密集型。计算密集型任务的特点是要进行大量的计算，消耗CPU资源，
比如对视频进行编码解码或者格式转换等等，这种任务全靠CPU的运算能力，虽然也可以用多任务完成，但是任务越多，花在任务切换的时间就越多，
CPU执行任务的效率就越低。计算密集型任务由于主要消耗CPU资源，这类任务用Python这样的脚本语言去执行效率通常很低，最能胜任这类任务的是C语言，
我们之前提到过Python中有嵌入C/C++代码的机制。

除了计算密集型任务，其他的涉及到网络、存储介质I/O的任务都可以视为I/O密集型任务，这类任务的特点是CPU消耗很少，
任务的大部分时间都在等待I/O操作完成（因为I/O的速度远远低于CPU和内存的速度）。对于I/O密集型任务，如果启动多任务，
就可以减少I/O等待时间从而让CPU高效率的运转。有一大类的任务都属于I/O密集型任务，这其中包括了我们很快会涉及到的网络应用和Web应用。


【单线程+异步I/O】
现代操作系统对I/O操作的改进中最为重要的就是支持异步I/O。如果充分利用操作系统提供的异步I/O支持，就可以用单进程单线程模型来执行多任务，
这种全新的模型称为事件驱动模型。Nginx就是支持异步I/O的Web服务器，它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上，
可以运行多个进程（数量与CPU核心数相同），充分利用多核CPU。用Node.js开发的服务器端程序也使用了这种工作模式，这也是当下并发编程的一种流行方案。

在Python语言中，单线程+异步I/O的编程模型称为协程，有了协程的支持，就可以基于事件驱动编写高效的多任务程序。协程最大的优势就是极高的执行效率，
因为子程序切换不是线程切换，而是由程序自身控制，因此，没有线程切换的开销。协程的第二个优势就是不需要多线程的锁机制，因为只有一个线程，
也不存在同时写变量冲突，在协程中控制共享资源不用加锁，只需要判断状态就好了，所以执行效率比多线程高很多。如果想要充分利用CPU的多核特性，
最简单的方法是多进程+协程，既充分利用多核，又充分发挥协程的高效率，可获得极高的性能。


重点：多线程和多进程的比较。

以下情况需要使用多线程：

程序需要维护许多共享的状态（尤其是可变状态），Python中的列表、字典、集合都是线程安全的，所以使用线程而不是进程维护共享状态的代价相对较小。
程序会花费大量时间在I/O操作上，没有太多并行计算的需求且不需占用太多的内存。
以下情况需要使用多进程：

程序执行计算密集型任务（如：字节码操作、数据处理、科学计算）。
程序的输入可以并行的分成块，并且可以将运算结果合并。
程序在内存使用方面没有任何限制且不强依赖于I/O操作（如：读写文件、套接字等）。
'''
from asyncio import Queue
from multiprocessing import Process
from threading import Thread

# todo 1、例子1：将耗时间的任务放到线程中以获得更好的用户体验。

'''
如下所示的界面中，有“下载”和“关于”两个按钮，用休眠的方式模拟点击“下载”按钮会联网下载文件需要耗费10秒的时间，如果不使用“多线程”，
我们会发现，当点击“下载”按钮后整个程序的其他部分都被这个耗时间的任务阻塞而无法执行了，这显然是非常糟糕的用户体验，代码如下所示。
'''

import time
import tkinter.messagebox
import tkinter as tk
from multiprocessing import Process, Queue
from random import randint
from time import time


def download():
    # 模拟下载任务需要花费10秒钟时间
    time.sleep(10)
    tkinter.messagebox.showinfo('提示', '下载完成!')


def show_about():
    tkinter.messagebox.showinfo('关于', '人民万岁！')


def single_thread():
    # 运行该代码后，你会看到一个标题为 "单线程" 的窗口，大小为 200x150 像素，并且该窗口总是在其他所有窗口的前面。
    # 创建顶层窗口
    top = tk.Tk()

    # 设置窗口标题
    top.title('单线程')

    # 设置窗口大小
    top.geometry('200x150')

    # 设置窗口总在最前端
    top.wm_attributes('-topmost', True)

    # 创建一个框架（Frame），用于包含按钮
    panel = tk.Frame(top)

    # 创建第一个按钮，文本为“下载”，点击时调用 download 函数
    button1 = tk.Button(panel, text='下载', command=download)
    # 使用 pack 方法将按钮放置在框架的左侧
    button1.pack(side='left')

    # 创建第二个按钮，文本为“关于”，点击时调用 show_about 函数
    button2 = tk.Button(panel, text='关于', command=show_about)
    # 使用 pack 方法将按钮放置在框架的右侧
    button2.pack(side='right')

    # 将框架放置在窗口底部
    panel.pack(side='bottom')

    # 进入主事件循环
    top.mainloop()


def use_thread():
    # 定义一个继承自 Thread 的类，用于处理下载任务
    class DownloadTaskHandler(Thread):
        def run(self):
            # 模拟下载任务
            time.sleep(10)
            # 显示下载完成的信息框
            tkinter.messagebox.showinfo('提示', '下载完成!')
            # 启用下载按钮
            button1.config(state=tkinter.NORMAL)

    # 定义下载按钮的回调函数
    def download():
        # 禁用下载按钮
        # 目的是防止用户在下载任务进行期间重复点击按钮，避免启动多个并发的下载任务，同时也提高了用户体验。
        # 下载任务完成后，重新启用按钮，允许用户再次发起下载任务。
        button1.config(state=tkinter.DISABLED)
        # 创建并启动下载任务线程
        # 通过 daemon 参数将线程设置为守护线程（主程序退出时自动结束）会调用run()
        DownloadTaskHandler(daemon=True).start()

    # 定义关于按钮的回调函数
    def show_about():
        # 显示关于信息框
        tkinter.messagebox.showinfo('关于', '同志，你好！')

    # 创建主窗口
    top = tkinter.Tk()
    # 设置窗口标题
    top.title('单线程')
    # 设置窗口大小
    top.geometry('200x150')
    # 设置窗口总是在最前面
    top.wm_attributes('-topmost', 1)

    # 创建一个面板，用于放置按钮
    panel = tkinter.Frame(top)
    # 创建下载按钮，并将下载函数绑定到按钮
    button1 = tkinter.Button(panel, text='下载', command=download)
    # 使用 pack 布局管理器将按钮放置在面板的左侧
    button1.pack(side='left')
    # 创建关于按钮，并将关于函数绑定到按钮
    button2 = tkinter.Button(panel, text='关于', command=show_about)
    # 使用 pack 布局管理器将按钮放置在面板的右侧
    button2.pack(side='right')
    # 使用 pack 布局管理器将面板放置在窗口的底部
    panel.pack(side='bottom')

    # 启动主事件循环
    tkinter.mainloop()


# todo 例子2：使用多进程对复杂任务进行“分而治之”。
'''
完成成1~100000000求和的计算密集型任务
如果用循环的话：
from time import time


def main():
    total = 0
    number_list = [x for x in range(1, 100000001)]
    start = time()
    for number in number_list:
        total += number
    print(total)
    end = time()
    print('Execution time: %.3fs' % (end - start))


if __name__ == '__main__':
    main()
    
    
在上面的代码中，我故意先去创建了一个列表容器然后填入了100000000个数，这一步其实是比较耗时间的，所以为了公平起见，
当我们将这个任务分解到8个进程中去执行的时候，我们暂时也不考虑列表切片操作花费的时间，只是把做运算和合并运算结果的时间统计出来，
代码如下所示。

比较两段代码的执行结果（在我目前使用的MacBook上，上面的代码需要大概6秒左右的时间，而下面的代码只需要不到1秒的时间，再强调一次我们只是比较了运算的时间，
不考虑列表创建及切片操作花费的时间），使用多进程后由于获得了更多的CPU执行时间以及更好的利用了CPU的多核特性，明显的减少了程序的执行时间，而且计算量越大效果越明显。
当然，如果愿意还可以将多个进程部署在不同的计算机上，做成分布式进程，具体的做法就是通过multiprocessing.managers模块中提供的管理器将Queue对象通过网络共享出来（注册到网络上让其他计算机可以访问）    

'''


def task_handler(curr_list, result_queue):
    """
    处理任务的函数，计算当前列表的总和并将结果放入队列中。

    参数:
    curr_list (list): 要处理的数字列表。
    result_queue (Queue): 用于存储结果的队列。
    """
    total = 0
    # 遍历当前列表，计算总和
    for number in curr_list:
        total += number
    # 将结果放入队列中
    result_queue.put(total)


def use_process():
    processes = []  # 用于存储进程的列表
    number_list = [x for x in range(1, 100000001)]  # 生成从1到100,000,000的数字列表
    result_queue = Queue()  # 创建一个队列用于存储每个进程的结果
    index = 0  # 切片索引初始值

    # 启动8个进程将数据切片后进行运算
    for _ in range(8):
        # 切分列表，每个进程处理12,500,000个数字
        p = Process(target=task_handler,
                    args=(number_list[index:index + 12500000], result_queue))
        index += 12500000  # 更新切片索引
        processes.append(p)  # 将进程对象添加到进程列表中
        p.start()  # 启动进程

    # 开始记录所有进程执行完成花费的时间
    start = time()  # 记录开始时间

    # 等待所有进程完成
    for p in processes:
        p.join()  # 等待进程p完成

    # 合并执行结果
    total = 0
    # 从队列中获取每个进程的结果并累加
    while not result_queue.empty():
        total += result_queue.get()

    print(total)  # 打印总和
    end = time()  # 记录结束时间
    print('Execution time: ', (end - start), 's', sep='')  # 打印执行时间


def main():
    # 单线程
    # single_thread()
    # 多线程
    # use_thread()
    # 多进程
    use_process()


if __name__ == '__main__':
    main()
